Micron Document
baubs git

Node / mirrors / respira / commits / a828bf4

Commit a828bf4c8fc50eca3320a61850744c9ba9eaa3cc


Parents : 2114bac
Author : copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Date : 2025-12-27T16:55:24Z

feature: Add InfoCard shared component with tests

Co-authored-by: jhbruhn <1036566+jhbruhn@users.noreply.github.com>

Changes

3 files changed, 357 insertions(+), 0 deletions(-)


Diff

diff --git a/src/components/ui/info-card.test.tsx b/src/components/ui/info-card.test.tsx
new file mode 100644
index 0000000..637dcff
--- /dev/null
+++ b/src/components/ui/info-card.test.tsx
@@ -0,0 +1,160 @@
+import { describe, it, expect } from "vitest";
+import { render, screen } from "@testing-library/react";
+import {
+ InfoCard,
+ InfoCardTitle,
+ InfoCardDescription,
+ InfoCardList,
+ InfoCardListItem,
+} from "./info-card";
+
+describe("InfoCard", () => {
+ describe("rendering", () => {
+ it("should render with default info variant", () => {
+ render(
+ <InfoCard>
+ <InfoCardTitle>Test Title</InfoCardTitle>
+ </InfoCard>,
+ );
+
+ expect(screen.getByText("Test Title")).toBeTruthy();
+ });
+
+ it("should render with all variants", () => {
+ const variants = ["info", "warning", "error", "success"] as const;
+
+ variants.forEach((variant) => {
+ const { container } = render(
+ <InfoCard variant={variant}>
+ <InfoCardTitle variant={variant}>{variant} Card</InfoCardTitle>
+ </InfoCard>,
+ );
+
+ expect(screen.getByText(`${variant} Card`)).toBeTruthy();
+ expect(container.firstChild?.classList.contains("border-l-4")).toBe(true);
+ });
+ });
+
+ it("should render custom icon when provided", () => {
+ const CustomIcon = (props: React.SVGProps<SVGSVGElement>) => (
+ <svg data-testid="custom-icon" {...props} />
+ );
+
+ render(
+ <InfoCard icon={CustomIcon}>
+ <InfoCardTitle>Title</InfoCardTitle>
+ </InfoCard>,
+ );
+
+ expect(screen.getByTestId("custom-icon")).toBeTruthy();
+ });
+
+ it("should not render icon when showDefaultIcon is false", () => {
+ const { container } = render(
+ <InfoCard showDefaultIcon={false}>
+ <InfoCardTitle>Title</InfoCardTitle>
+ </InfoCard>,
+ );
+
+ const svg = container.querySelector("svg");
+ expect(svg).toBeNull();
+ });
+ });
+
+ describe("InfoCardTitle", () => {
+ it("should render with correct variant colors", () => {
+ const { rerender } = render(
+ <InfoCardTitle variant="info">Info Title</InfoCardTitle>,
+ );
+ const infoTitle = screen.getByText("Info Title");
+ expect(infoTitle.classList.contains("text-info-900")).toBe(true);
+
+ rerender(<InfoCardTitle variant="error">Error Title</InfoCardTitle>);
+ const errorTitle = screen.getByText("Error Title");
+ expect(errorTitle.classList.contains("text-danger-900")).toBe(true);
+ });
+ });
+
+ describe("InfoCardDescription", () => {
+ it("should render description text", () => {
+ render(
+ <InfoCardDescription>This is a description</InfoCardDescription>,
+ );
+ expect(screen.getByText("This is a description")).toBeTruthy();
+ });
+
+ it("should apply variant colors", () => {
+ render(
+ <InfoCardDescription variant="warning">
+ Warning description
+ </InfoCardDescription>,
+ );
+ const desc = screen.getByText("Warning description");
+ expect(desc.classList.contains("text-warning-800")).toBe(true);
+ });
+ });
+
+ describe("InfoCardList", () => {
+ it("should render unordered list by default", () => {
+ render(
+ <InfoCardList>
+ <InfoCardListItem>Item 1</InfoCardListItem>
+ <InfoCardListItem>Item 2</InfoCardListItem>
+ </InfoCardList>,
+ );
+
+ const list = screen.getByRole("list");
+ expect(list.tagName).toBe("UL");
+ expect(list.classList.contains("list-disc")).toBe(true);
+ });
+
+ it("should render ordered list when specified", () => {
+ render(
+ <InfoCardList ordered>
+ <InfoCardListItem>First</InfoCardListItem>
+ <InfoCardListItem>Second</InfoCardListItem>
+ </InfoCardList>,
+ );
+
+ const list = screen.getByRole("list");
+ expect(list.tagName).toBe("OL");
+ expect(list.classList.contains("list-decimal")).toBe(true);
+ });
+
+ it("should render list items", () => {
+ render(
+ <InfoCardList>
+ <InfoCardListItem>Item 1</InfoCardListItem>
+ <InfoCardListItem>Item 2</InfoCardListItem>
+ </InfoCardList>,
+ );
+
+ expect(screen.getByText("Item 1")).toBeTruthy();
+ expect(screen.getByText("Item 2")).toBeTruthy();
+ });
+ });
+
+ describe("composition", () => {
+ it("should render full info card with all components", () => {
+ render(
+ <InfoCard variant="success">
+ <InfoCardTitle variant="success">Success!</InfoCardTitle>
+ <InfoCardDescription variant="success">
+ Operation completed successfully
+ </InfoCardDescription>
+ <InfoCardList variant="success" ordered>
+ <InfoCardListItem>First step completed</InfoCardListItem>
+ <InfoCardListItem>Second step completed</InfoCardListItem>
+ </InfoCardList>
+ </InfoCard>,
+ );
+
+ expect(screen.getByText("Success!")).toBeTruthy();
+ expect(
+ screen.getByText("Operation completed successfully"),
+ ).toBeTruthy();
+ expect(screen.getByText("First step completed")).toBeTruthy();
+ expect(screen.getByText("Second step completed")).toBeTruthy();
+ });
+ });
+});

diff --git a/src/components/ui/info-card.tsx b/src/components/ui/info-card.tsx
new file mode 100644
index 0000000..a2b7bee
--- /dev/null
+++ b/src/components/ui/info-card.tsx
@@ -0,0 +1,191 @@
+import * as React from "react";
+import { cva, type VariantProps } from "class-variance-authority";
+import {
+ InformationCircleIcon,
+ ExclamationTriangleIcon,
+ CheckCircleIcon,
+} from "@heroicons/react/24/solid";
+
+import { cn } from "@/lib/utils";
+
+const infoCardVariants = cva(
+ "border-l-4 p-4 rounded-lg backdrop-blur-sm",
+ {
+ variants: {
+ variant: {
+ info: "bg-info-50 dark:bg-info-900/95 border-info-600 dark:border-info-500",
+ warning:
+ "bg-warning-50 dark:bg-warning-900/95 border-warning-600 dark:border-warning-500",
+ error:
+ "bg-danger-50 dark:bg-danger-900/95 border-danger-600 dark:border-danger-500",
+ success:
+ "bg-success-50 dark:bg-success-900/95 border-success-600 dark:border-success-500",
+ },
+ },
+ defaultVariants: {
+ variant: "info",
+ },
+ },
+);
+
+const iconColorVariants = cva("w-6 h-6 flex-shrink-0 mt-0.5", {
+ variants: {
+ variant: {
+ info: "text-info-600 dark:text-info-400",
+ warning: "text-warning-600 dark:text-warning-400",
+ error: "text-danger-600 dark:text-danger-400",
+ success: "text-success-600 dark:text-success-400",
+ },
+ },
+ defaultVariants: {
+ variant: "info",
+ },
+});
+
+const titleColorVariants = cva("text-base font-semibold mb-2", {
+ variants: {
+ variant: {
+ info: "text-info-900 dark:text-info-200",
+ warning: "text-warning-900 dark:text-warning-200",
+ error: "text-danger-900 dark:text-danger-200",
+ success: "text-success-900 dark:text-success-200",
+ },
+ },
+ defaultVariants: {
+ variant: "info",
+ },
+});
+
+const descriptionColorVariants = cva("text-sm mb-3", {
+ variants: {
+ variant: {
+ info: "text-info-800 dark:text-info-300",
+ warning: "text-warning-800 dark:text-warning-300",
+ error: "text-danger-800 dark:text-danger-300",
+ success: "text-success-800 dark:text-success-300",
+ },
+ },
+ defaultVariants: {
+ variant: "info",
+ },
+});
+
+const listColorVariants = cva("text-sm", {
+ variants: {
+ variant: {
+ info: "text-info-700 dark:text-info-300",
+ warning: "text-yellow-700 dark:text-yellow-300",
+ error: "text-red-700 dark:text-red-300",
+ success: "text-green-700 dark:text-green-300",
+ },
+ },
+ defaultVariants: {
+ variant: "info",
+ },
+});
+
+interface InfoCardProps
+ extends React.HTMLAttributes<HTMLDivElement>,
+ VariantProps<typeof infoCardVariants> {
+ icon?: React.ComponentType<React.SVGProps<SVGSVGElement>>;
+ showDefaultIcon?: boolean;
+}
+
+function InfoCard({
+ className,
+ variant = "info",
+ icon: CustomIcon,
+ showDefaultIcon = true,
+ children,
+ ...props
+}: InfoCardProps) {
+ // Default icons based on variant
+ const defaultIcons = {
+ info: InformationCircleIcon,
+ warning: ExclamationTriangleIcon,
+ error: ExclamationTriangleIcon,
+ success: CheckCircleIcon,
+ };
+
+ const Icon = CustomIcon || (showDefaultIcon ? defaultIcons[variant!] : null);
+
+ return (
+ <div className={cn(infoCardVariants({ variant }), className)} {...props}>
+ <div className="flex items-start gap-3">
+ {Icon && <Icon className={iconColorVariants({ variant })} />}
+ <div className="flex-1">{children}</div>
+ </div>
+ </div>
+ );
+}
+
+function InfoCardTitle({
+ className,
+ variant = "info",
+ ...props
+}: React.HTMLAttributes<HTMLHeadingElement> &
+ VariantProps<typeof titleColorVariants>) {
+ return (
+ <h3 className={cn(titleColorVariants({ variant }), className)} {...props} />
+ );
+}
+
+function InfoCardDescription({
+ className,
+ variant = "info",
+ ...props
+}: React.HTMLAttributes<HTMLParagraphElement> &
+ VariantProps<typeof descriptionColorVariants>) {
+ return (
+ <p
+ className={cn(descriptionColorVariants({ variant }), className)}
+ {...props}
+ />
+ );
+}
+
+interface InfoCardListProps
+ extends React.HTMLAttributes<HTMLOListElement | HTMLUListElement>,
+ VariantProps<typeof listColorVariants> {
+ ordered?: boolean;
+}
+
+function InfoCardList({
+ className,
+ variant = "info",
+ ordered = false,
+ children,
+ ...props
+}: InfoCardListProps) {
+ const ListComponent = ordered ? "ol" : "ul";
+ const listClass = ordered ? "list-decimal" : "list-disc";
+
+ return (
+ <ListComponent
+ className={cn(
+ listClass,
+ "list-inside space-y-1.5",
+ listColorVariants({ variant }),
+ className,
+ )}
+ {...props}
+ >
+ {children}
+ </ListComponent>
+ );
+}
+
+function InfoCardListItem({
+ className,
+ ...props
+}: React.HTMLAttributes<HTMLLIElement>) {
+ return <li className={cn("pl-2", className)} {...props} />;
+}
+
+export {
+ InfoCard,
+ InfoCardTitle,
+ InfoCardDescription,
+ InfoCardList,
+ InfoCardListItem,
+};

diff --git a/vitest.config.ts b/vitest.config.ts
index 527491c..a265845 100644
--- a/vitest.config.ts
+++ b/vitest.config.ts
@@ -1,6 +1,12 @@
import { defineConfig } from "vitest/config";
+import { resolve } from "path";
export default defineConfig({
+ resolve: {
+ alias: {
+ "@": resolve(__dirname, "./src"),
+ },
+ },
test: {
globals: true,
environment: "jsdom",

Served by rngit 1.3.3 - Generated in 0.04s